package com.ljc.db.backend.dm.logger;

import com.google.common.primitives.Bytes;
import com.ljc.db.backend.utils.Panic;
import com.ljc.db.backend.utils.Parser;
import com.ljc.db.common.Error;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 日志文件读写
 * 
 * 日志文件标准格式为：
 * [XChecksum] [Log1] [Log2] ... [LogN] [BadTail]
 * XChecksum 是对后续所有日志计算的Checksum，int类型
 * 
 * 每条正确日志的格式为：
 * [Size] [Checksum] [Data]
 * 0-4    4-8         8-~
 * Size 4字节int 标识Data长度
 * Checksum 4字节int
 */
public class LoggerImpl implements Logger {

    private static final int SEED = 13331; // 计算校验和的种子

    private static final int OF_SIZE = 0; //日志中size部分的偏移量
    private static final int OF_CHECKSUM = OF_SIZE + 4; // 日志中Checksum部分的偏移量
    private static final int OF_DATA = OF_CHECKSUM + 4; // 日志中Data部分的偏移量
    
    public static final String LOG_SUFFIX = ".log";

    private RandomAccessFile file;
    private FileChannel fc;
    private Lock lock;

    private long position;  // 当前日志指针的位置
    private long fileSize;  // 初始化时记录，log操作不更新
    private int xChecksum;  // 日志文件的校验和

    LoggerImpl(RandomAccessFile raf, FileChannel fc) {
        this.file = raf;
        this.fc = fc;
        lock = new ReentrantLock();
    }

    LoggerImpl(RandomAccessFile raf, FileChannel fc, int xChecksum) {
        this.file = raf;
        this.fc = fc;
        this.xChecksum = xChecksum;
        lock = new ReentrantLock();
    }

    void init() {
        long size = 0;
        try {
            size = file.length();
        } catch (IOException e) {
            Panic.panic(e);
        }
        if(size < 4) {
            Panic.panic(Error.BadLogFileException);
        }

        ByteBuffer raw = ByteBuffer.allocate(4);
        try {
            fc.position(0);
            fc.read(raw);
        } catch (IOException e) {
            Panic.panic(e);
        }
        int xChecksum = Parser.parseInt(raw.array());
        this.fileSize = size;
        this.xChecksum = xChecksum;

        checkAndRemoveTail();
    }

    /**
     * 在最开始打开日志文件时，检查整个.log文件的校验和并移除bad tail
     * 【bad tail可以直接移除，它没有记录完成并不是完整的日志，在internNext()中会直接返回null，没有算在整个文件的校验和中】
     */
    private void checkAndRemoveTail() {
        // 将指针指向.log【日志文件】中的第一条日志
        rewind();

        int xCheck = 0;
        // 遍历所有日志，计算所有日志的校验和
        while(true) {
            // 调用internNext()方法会移动position，方便后面截取日志文件
            byte[] log = internNext();
            if(log == null) break;
            xCheck = calChecksum(xCheck, log);
        }
        // 判断当前日志文件的校验和是否匹配，不匹配说明日志被篡改，抛出异常
        if(xCheck != xChecksum) {
            Panic.panic(Error.BadLogFileException);
        }

        try {
            // 截断日志文件到正常日志的末尾
            truncate(position);
        } catch (Exception e) {
            Panic.panic(e);
        }
        try {
            // 将读写文件的指针指向正常日志的末尾，判断截取是否成功？
            file.seek(position);
        } catch (IOException e) {
            Panic.panic(e);
        }
        // 将指针指向.log【日志文件】中的第一条日志
        rewind();
    }

    /**
     * 计算每个日志的校验和
     */
    private int calChecksum(int xCheck, byte[] log) {
        for (byte b : log) {
            xCheck = xCheck * SEED + b;
        }
        return xCheck;
    }

    /**
     * 记录日志信息
     */
    @Override
    public void log(byte[] data) {
        // 将数据包装为日志返回
        byte[] log = wrapLog(data);
        ByteBuffer buf = ByteBuffer.wrap(log);
        lock.lock();
        try {
            // 向通道中记录日志【会在更新校验和的时候同步将数据持久化到磁盘，防止程序中断，导致校验和与日志数据不对应】
            fc.position(fc.size());
            fc.write(buf);
        } catch(IOException e) {
            Panic.panic(e);
        } finally {
            lock.unlock();
        }
        // 更新校验和
        updateXChecksum(log);
    }

    /**
     * 更新整个日志文件的校验和【并将日志信息和更新后的校验和一起持久化到磁盘】
     */
    private void updateXChecksum(byte[] log) {
        // 计算新校验和
        this.xChecksum = calChecksum(this.xChecksum, log);
        try {
            // 将新校验和覆盖到日志文件头部，并持久化到磁盘
            fc.position(0);
            fc.write(ByteBuffer.wrap(Parser.int2Byte(xChecksum)));
            fc.force(false);
        } catch(IOException e) {
            Panic.panic(e);
        }
    }

    /**
     * 将要写的数据包装成日志格式
     */
    private byte[] wrapLog(byte[] data) {
        // 计算校验和
        byte[] checksum = Parser.int2Byte(calChecksum(0, data));
        // 计算数据大小
        byte[] size = Parser.int2Byte(data.length);
        // 将这些数据组合成数组返回
        return Bytes.concat(size, checksum, data);
    }

    /**
     *  阶段日志文件到正常日志的末尾
     */
    @Override
    public void truncate(long x) throws Exception {
        lock.lock();
        try {
            fc.truncate(x);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 读取日志文件中的日志
     */
    private byte[] internNext() {
        // 如果当前指针位置加上数据偏移量大于.log【日志文件】文件大小，返回null，表示日志已经读取完毕
        if(position + OF_DATA >= fileSize) {
            return null;
        }
        // 否则就读取本条日志的大小
        ByteBuffer tmp = ByteBuffer.allocate(4);
        try {
            fc.position(position);
            fc.read(tmp);
        } catch(IOException e) {
            Panic.panic(e);
        }
        int size = Parser.parseInt(tmp.array());
        // 如果本条当前指针位置【此时指针还指向上一条日志的末尾】 + 日志的大小 + 数据偏移量大于.log【日志文件首地址】大小，返回null，表示没有日志数据
        if(position + size + OF_DATA > fileSize) {
            return null;
        }
        // 否则就读取整个日志【OF_DATA表示读取到数据前的所有内容，size表示读取数据大小那么多的内容】
        ByteBuffer buf = ByteBuffer.allocate(OF_DATA + size);
        try {
            fc.position(position);
            fc.read(buf);
        } catch(IOException e) {
            Panic.panic(e);
        }

        byte[] log = buf.array();
        // 使用种子计算当前log里面的数据
        int checkSum1 = calChecksum(0, Arrays.copyOfRange(log, OF_DATA, log.length));
        // 直接读出当前log的校验和
        int checkSum2 = Parser.parseInt(Arrays.copyOfRange(log, OF_CHECKSUM, OF_DATA));
        // 比对两个校验和是否相等，不相等证明当前日志被篡改过，直接返回
        if(checkSum1 != checkSum2) {
            return null;
        }
        // 否则就更新指针位置，为读取下一条日志做准备
        position += log.length;
        return log;
    }

    /**
     * 返回下一条日志的数据
     */
    @Override
    public byte[] next() {
        lock.lock();
        try {
            byte[] log = internNext();
            if(log == null) return null;
            // 返回日志数据
            return Arrays.copyOfRange(log, OF_DATA, log.length);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 让指针position指向第一条日志
     */
    @Override
    public void rewind() {
        position = 4;
    }

    @Override
    public void close() {
        try {
            fc.close();
            file.close();
        } catch(IOException e) {
            Panic.panic(e);
        }
    }
    
}
